﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml;
using System.Xml.Serialization;
using Microsoft.WowAddonStudio.FrameXml.Serialization.LayoutFrames;

namespace Microsoft.WowAddonStudio.FrameXml.Serialization.Support
{
    public class FrameXmlDirectory
    {
        #region Singleton

        private static readonly object syncRoot = new object();

        private static FrameXmlDirectory instance;
        private static FrameXmlDirectory explicitInstance;

        /// <summary>
        /// Gets or sets the default singleton instance.
        /// </summary>
        /// <remarks>
        /// When an instance is explicitly set, it is returned as the default instance as long as it is not set to <c>null</c>. A valid non-null instance is always
        /// returned (singleton instance implicitly created).
        /// </remarks>
        public static FrameXmlDirectory Default
        {
            get
            {
                if (explicitInstance != null)
                    return explicitInstance;
                
                if (instance == null)
                {
                    lock (syncRoot)
                    {
                        if (instance == null)
                            instance = new FrameXmlDirectory();
                    }
                }

                return instance;
            }
            set { explicitInstance = value; }
        }

        #endregion

        private readonly Dictionary<string, InheritableObject> inheritableObjects = new Dictionary<string, InheritableObject>();

        /// <summary>
        /// Gets the number of inheritable object registered.
        /// </summary>
        public int Count
        {
            get { return inheritableObjects.Count; }
        }


		/// <summary>
		/// Gets the names.
		/// </summary>
		/// <returns></returns>
		public IEnumerable<string> GetNames()
		{
            return inheritableObjects.Keys.OfType<string>();
		}

        /// <summary>
        /// Gets the names.
        /// </summary>
        /// <typeparam name="TInheritableObject">The type of the inheritable object.</typeparam>
        /// <returns></returns>
        public IEnumerable<string> GetNames<TInheritableObject>() where TInheritableObject : InheritableObject
        {
            foreach (var pair in inheritableObjects)
            {
                if (pair.Value.CanBeInheritedFrom(typeof(TInheritableObject)))
                    yield return pair.Key;
            }
        }

        /// <summary>
        /// Registers an inheritable object under its new name.
        /// </summary>
        /// <param name="inheritableObject">The inheritable object.</param>
        /// <param name="oldName">The old name of the inheritable object.</param>
        public void Register(InheritableObject inheritableObject, string oldName)
        {
            if (inheritableObject == null)
                throw new ArgumentNullException("inheritableObject");
            if (String.IsNullOrEmpty(inheritableObject.Name))
                throw new ArgumentException("Object has an invalid name.", "inheritableObject");

            // Check if we have an old name, remove entry if registered
            if (!String.IsNullOrEmpty(oldName) && inheritableObjects.ContainsKey(oldName))
                inheritableObjects.Remove(oldName);

            // Add inheritable object under new name
            inheritableObjects[inheritableObject.Name] = inheritableObject;
        }

        /// <summary>
        /// Registers an inheritable object.
        /// </summary>
        /// <param name="inheritableObject">The inheritable object.</param>
        public void Register(InheritableObject inheritableObject)
        {
            this.Register(inheritableObject, null);
        }

        /// <summary>
        /// Unregisters an inheritable object.
        /// </summary>
        /// <param name="inheritableObject">The inheritable object.</param>
        public void Unregister(InheritableObject inheritableObject)
        {
            if (inheritableObject == null)
                throw new ArgumentNullException("inheritableObject");

            if (inheritableObjects.ContainsKey(inheritableObject.Name))
                inheritableObjects.Remove(inheritableObject.Name);
        }

        /// <summary>
        /// Returns a value indicating whether an inherited object with a given name is registered.
        /// </summary>
        /// <param name="inheritableObjectName">The name of the inherited object.</param>
        /// <returns>True, if an inherited object is registered under the name; false otherwise.</returns>
        public bool IsRegistered(string inheritableObjectName)
        {
            if (inheritableObjectName == null)
                throw new ArgumentNullException("inheritableObjectName");

            return inheritableObjects.ContainsKey(inheritableObjectName);
        }

        /// <summary>
        /// Returns a value indicating whether an inherited object with a given name and type is registered.
        /// </summary>
        /// <typeparam name="TInheritableObject">The type of the inherited object.</typeparam>
        /// <param name="inheritableObjectName">The name of the inherited object.</param>
        /// <returns>True, if an inherited object is registered under the name; false otherwise.</returns>
        public bool IsRegistered<TInheritableObject>(string inheritableObjectName) where TInheritableObject : InheritableObject
        {
            // TODO: See if we should return true for subtypes
            return this.IsRegistered(inheritableObjectName) &&
                   inheritableObjects[inheritableObjectName].GetType() == typeof (TInheritableObject);
        }

        /// <summary>
        /// Resolves an inheritable object by its name.
        /// </summary>
        /// <param name="inheritableObjectName">The name of the inheritable object.</param>
        /// <returns>An instance of the <see cref="LayoutFrame"/> class, if found; <c>null</c> otherwise.</returns>
        public InheritableObject Resolve(string inheritableObjectName)
        {
            if (inheritableObjectName == null)
                throw new ArgumentNullException("inheritableObjectName");

            if (inheritableObjects.ContainsKey(inheritableObjectName))
                return inheritableObjects[inheritableObjectName];

            return null;
        }

        /// <summary>
        /// Resolves an inheritable object by its name.
        /// </summary>
        /// <typeparam name="TInheritableObject">The type of the inheritable object.</typeparam>
        /// <param name="inheritableObjectName">The name of the inheritable object.</param>
        /// <returns>An instance of the <typeparamref name="TInheritableObject"/> class.</returns>
        public TInheritableObject Resolve<TInheritableObject>(string inheritableObjectName) where TInheritableObject : InheritableObject
        {
            return (TInheritableObject)this.Resolve(inheritableObjectName);
        }

		/// <summary>
		/// Gets the FrameXML filenames inside a TOC file.
		/// </summary>
		/// <param name="tocFileStream">The stream for the TOC file.</param>
		/// <returns>An enumerable collection of the FrameXML filenames.</returns>
    	public static IEnumerable<string> GetFrameXmlFiles(Stream tocFileStream)
    	{
    		if (tocFileStream == null)
    			throw new ArgumentNullException("tocFileStream");

    		using (var streamReader = new StreamReader(tocFileStream))
    		{
    			while (!streamReader.EndOfStream)
    			{
    				// Read the next line from the TOC file
    				string tocLine = streamReader.ReadLine();

    				// Make sure line is not a comment or metadata and the file's extension is .xml
    				if (!tocLine.StartsWith("#") && Path.GetExtension(tocLine) == ".xml")
    				{
    					yield return tocLine.Trim();
    				}
    			}
    		}
    	}

        public delegate Stream GetFrameXmlStreamDelegate(string frameXmlFile);

    	/// <summary>
		/// Register the inheritable objects using an <see cref="XmlReader"/>.
		/// </summary>
    	/// <param name="frameXmlFile"></param>
    	/// <param name="getFrameXmlStream"></param>
    	public static void RegisterFrameXmlFile(string frameXmlFile, GetFrameXmlStreamDelegate getFrameXmlStream)
    	{
            if (frameXmlFile == null)
                throw new ArgumentNullException(("frameXmlFile"));
            if (getFrameXmlStream == null)
                throw new ArgumentNullException("getFrameXmlStream");

            using (var frameXmlStream = getFrameXmlStream(frameXmlFile))
            {
                if (frameXmlStream == null)
                    return;

                FrameXmlDirectory.RegisterFrameXmlFile(frameXmlStream, getFrameXmlStream);
            }
    	}

        /// <summary>
        /// Register the inheritable objects using an <see cref="XmlReader"/>.
        /// </summary>
        /// <param name="frameXmlStream"></param>
        /// <param name="getFrameXmlStream"></param>
        private static void RegisterFrameXmlFile(Stream frameXmlStream, GetFrameXmlStreamDelegate getFrameXmlStream)
        {
            if (frameXmlStream == null)
                throw new ArgumentNullException(("frameXmlStream"));

            var serializer = new XmlSerializer(typeof(Ui));

            using (var frameXmlReader = XmlReader.Create(frameXmlStream, new XmlReaderSettings { IgnoreComments = true }))
            {
                // Make sure the FrameXML file can be deserialized
                if (serializer.CanDeserialize(frameXmlReader))
                {
                    // Deserialize the root Ui element in the FrameXML file (this inherently registers the virtual inheritable objects)
                    var ui = (Ui)serializer.Deserialize(frameXmlReader);

                    // Initialize the parents
                    ui.InitializeParent();

                    // Handle <Include />s in the Ui
                    if (ui.Includes != null && getFrameXmlStream != null)
                    {
                        foreach (var include in ui.Includes)
                            FrameXmlDirectory.RegisterFrameXmlFile(include.File, getFrameXmlStream);
                    }
                }
            }
        }

    }
}
